/* 
 * sCanvas
 * JavaScript Framework for HTML5 Canvas
 * 
 * Copyright 2013 bielda
 * 
 * For the full copyright and license information, please view
 * the file license.txt that was distributed with this source code.
 * 
 * If there is no such file, the MIT License applies (http://opensource.org/licenses/MIT).
 */

(function(window) {
	
//Paul Irish's requestAnimationFrame polyfill (slightly modyfied)
//http://paulirish.com/2011/requestanimationframe-for-smart-animating/
if (typeof window.requestAnimationFrame !== "function") {
	window.requestAnimationFrame = (function() {
		return window.webkitRequestAnimationFrame
			|| window.mozRequestAnimationFrame
			|| window.oRequestAnimationFrame
			|| window.msRequestAnimationFrame
			|| function(callback){
				window.setTimeout(callback, 1000 / 60);
			};
	}());
}

var 
	//regular expression for valid object ID
	idRegex = /[a-zA-Z_][a-zA-Z0-9_]*/,

	//constructor
	sCanvas = function(selector, ready, error) {
		//self-reference
		var loaded,
			sc = this;

		//check selector
		if (typeof selector !== "string" && !selector.nodeType) {
			throw new TypeError("Selector must be string or HTMLElement.");
		}

		//save selector
		this.selector = selector;

		//initialize firlds to its default values
		this.initFields();

		//bind primary ready handler
		this.ready(this.init);

		//bind passed handlers
		if (typeof ready === "function") {
			this.ready(ready);
		}

		if (typeof error === "function") {
			this.error(error);
		}

		//if DOM was already loaded, fire ready immediatelly
		if (document.readyState === "complete") {
			this.ready();
		} else {
			loaded = function() {
				window.removeEventListener("load", loaded, false);
				document.removeEventListener("DOMContentLoaded", loaded, false);
				sc.ready();
			};

			//better way to detect DOM load
			document.addEventListener("DOMContentLoaded", loaded, false);

			//load will always work
			window.addEventListener("load", loaded, false);
		}
	};

sCanvas.fn = sCanvas.prototype;

//extends first object with one or more others
sCanvas.extend = sCanvas.fn.extend = function() {
	var key, obj,
		prototypeExtended = false,
		result = arguments[0] || { },
		deep = false,
		i = 1;
	
	if (typeof result === "boolean") {
		deep = result;
		result = arguments[1] || { };
		i++;
	}
	
	//if only one object is passed, sCanvas will be extended
	if (arguments.length === i) {
		result = this;
		i--;
		
		if (this === sCanvas.fn) {
			prototypeExtended = true;
		}
	} else if (typeof result !== "object") {
		result = { };
	}
	
	for (; i < arguments.length; i++) {
		obj = arguments[i];
		if (obj !== undefined && obj !== null) {
			if (deep) {
				for (key in obj) {
					if (typeof obj[key] === "object" && obj[key] !== null) {
						//if extending prototype with object, defer it for object creation
						if (prototypeExtended) {
							sCanvas.fields[key] = sCanvas.fields[key] || (sCanvas.isArray(obj[key]) ? [] : {});
							sCanvas.extend(true, sCanvas.fields[key], obj[key]);
						} else {
							result[key] = result[key] || (sCanvas.isArray(obj[key]) ? [] : {});
							sCanvas.extend(true, result[key], obj[key]);
						}
					} else {
						result[key] = obj[key];
					}
				}
			} else {
				for (key in obj) {
					//if extending prototype with object, defer it for object creation
					if (prototypeExtended && typeof obj[key] === "object") {
						sCanvas.fields[key] = obj[key];
					} else {
						result[key] = obj[key];
					}
				}
			}
		}
	}

	return result;
};

sCanvas.extend({
	
	fields: { },
	
	//returns actuall time as a number
	now: function() {
		return (new Date).getTime();
	},

	//checks if passed argument is valid ID, returns true if it is valid
	isValidId: function(id) {
		return id.match(idRegex) !== null;
	},

	//checks if passed object is group
	isGroup: function(obj) {
		return (obj instanceof CanvasGroup);
	},
	
	//checks if passed object is Array
	isArray: function(obj) {
		if (typeof Array.isArray === "function") {
			return Array.isArray(obj);
		} else {
			return Object.prototype.toString.apply(obj, [ ]) === "[object Array]";
		}
	},
	
	//returns true if passed object is empty object or empty array
	isEmpty: function(obj) {
		var i, result = true;
		if (typeof obj === "object") {
			for (i in obj) {
				result = false;
				break;
			}
		} else {
			result = false;
		}
		return result;
	},
	
	//constructs event object
	Event: function(type, data) {
		return new EventObject(type, data);
	}
	
});

sCanvas.fn.extend(true, {

	//reassigns constructor
//		constructor: sCanvas,

	//stores original canvas selector passed to constructor
	selector: null,

	//stores elemement wrapping canvas
	wrapper: null,

	//stores front canvas
	front: null,

	//stores back canvas
	back: null,

	//stores context of front canvas
	frontCx: null,

	//stores context of back canvas
	backCx: null,

	//stores event handlers
	handlers: {
		ready: [ ],
		error: [ ]
	},

	//remembers if canvas is already ready
	isReady: false,

	//remembers if canvas is already initialized with error
	isError: false,

	//stores if canvas is autorepainting
	autorepainting: true,

	//stores canvas position in element
	pos: {
		top: 0,
		left: 0
	},
	
	//stores if canvas was already initialized
	initialized: false,
	
	//initializes object fields
	initFields: function() {
		if (!this.initialized) {
			sCanvas.extend(true, this, sCanvas.fields);
			this.initialized = true;
		}
	},

	//initializes sCanvas
	//is invoked as first ready handler
	init: function(e) {
		var err = null;

		//get wrapper element and save it
		if (typeof this.selector === "string") {
			this.wrapper = document.getElementById(this.selector);
		} else if (this.selector.nodeType) {
			this.wrapper = this.selector;
		}

		//get canvas - do not raise error when canvas was not 
		try {
			this.front = this.wrapper.getElementsByTagName("canvas")[0];
		} catch (err) {}

		//check if canvas was found
		if (!!this.front) {
			//get context and save it
			this.frontCx = this.front.getContext("2d");

			//check if context was created properly
			if (!!this.frontCx) {
				this.pos = this.position();
				this.wrap();
			} else {
				this.frontCx = null;
				err = new Error("This browser does not support canvas.");
			}
		} else {
			err = new ReferenceError("Canvas element was not found.");
		}

		//if error occured, stop propagating ready event and trigger error event
		if (!!err) {
			e.stopImmediatePropagation();
			
			this.trigger(
				sCanvas.Event("error", {
					error: err
				}).removeHandlers()
			);
		} else {
			//sCanvas was initialized properly
			//so there is no need to store error handlers anymore
			this.handlers.error = [ ];
		}
	},

	//creates html structure, adds necessary styles
	wrap: function() {
		var wrapperStyles = window.getComputedStyle(this.wrapper);

		//make wrapper offset parent
		if (wrapperStyles.getPropertyValue("position") === "static") {
			this.wrapper.style.position = "relative";
		}

		//add canvas absolute position styles
		this.front.style.position = "absolute";
		this.front.style.top = "0px";
		this.front.style.left = "0px";
		this.front.style.zIndex = "2";

		//clone original canvas
		this.back = this.front.cloneNode();
		this.back.style.zIndex = "1";
		this.backCx = this.back.getContext("2d");
		this.wrapper.appendChild(this.back);
	},

	//counts canvas position in element
	position: function() {
		var left = 0,
			top = 0,
			obj = this.front;

		while (obj !== null) {
			left += obj.offsetLeft;
			top += obj.offsetTop;

			obj = obj.offsetParent;
		}

		return {
			left: left,
			top: top
		};
	},

	//registers ready event handler or triggers ready event
	ready: function(fn) {
		if (fn === undefined) {
			//triggering event
			this.trigger(
				sCanvas.Event("ready").removeHandlers()
			);

			//remember that ready event has been already triggered
			this.isReady = true;
		} else {
			//registering handler
			//if ready already occured, execute it immeditelly
			if (this.isReady) {
				this.bind("ready", fn).trigger(
					sCanvas.Event("ready").removeHandlers()
				);
			} else {
				this.bind("ready", fn);
			}
		}
		return this;
	},

	//registers error event handler or triggers error event
	error: function(fn) {
		if (fn === undefined) {
			//triggering event
			this.trigger(
				sCanvas.Event("error").removeHandlers()
			);

			//remember that error event has been already triggered
			this.isError = true;
		} else {
			//registering handler
			//if error already occured, execute it immeditelly
			if (this.isError) {
				this.bind("error", fn).trigger(
					sCanvas.Event("error").removeHandlers()
				);
			} else {
				this.bind("error", fn);
			}
		}
		return this;
	},

	//binds event handler
	bind: function(event, fn) {
		if (typeof fn !== "function") {
			throw new TypeError("Event handler was not passed.");
		}

		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		this.handlers[event].push(fn);

		return this;
	},

	//triggers event
	//remove is internal argument, should not be used externally
	trigger: function(event, data) {
		var args, e, i;
		
		//unify arguments
		if (event instanceof EventObject) {
			e = event;
			event = event.type;
		} else {
			e = sCanvas.Event(event);
		}

		//check if passed event type is supported
		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		args = [ e ];
		if (sCanvas.isArray(data)) {
			for (i = 0; i < data.length; i++) {
				args.push(data[i]);
			}
		}

		//execute all binded handlers
		for (i in this.handlers[event]) {
			this.handlers[event][i].apply(this, args);

			//if executing of rest of the handlers was stopped
			if (e.isImmediatePropagationStopped()) {
				break;
			}
		}

		//if remove is set to true, handlers are removed
		if (e.isRemovingHandlers()) {
			this.handlers[event] = [ ];
		}

		return this;
	},

	//performs repaint if autorepainting is on
	autorepaint: function() {
		if (this.isAutorepainting()) {
			this.repaint();
		}
	},

	//returns true if autorepainting is on
	isAutorepainting: function() {
		return this.autorepainting;
	},
	
	//turns autorepainting on
	//if passed argument is true, canvas is also repainted
	startAutorepainting: function(repaint) {
		this.autorepainting = true;
		
		if (repaint) {
			this.repaint();
		}
	},
	
	//turns autorepainting off
	stopAutorepainting: function() {
		this.autorepainting = false;
	},

	//repaints canvas
	repaint: function() {
		var obj;

		//TODO: think of some optimization to canvas clearing
		this.frontCx.clearRect(0, 0, this.width(), this.height());

		obj = this.bottom;
		while (obj !== null) {
			obj.draw(this.frontCx);
			obj = obj.overObj;
		}

		return this;
	},

	//gets/sets canvas width
	width: function(w) {
		if (w === undefined) {
			//getter
			return this.front.width;
		} else {
			//setter
			//TODO: think of performing repaint
			this.front.width = w;
			this.back.width = w;
			return this;
		}
	},

	//gets/sets canvas width
	height: function(h) {
		if (h === undefined) {
			//getter
			return this.front.height;
		} else {
			//setter
			//TODO: think of performing repaint
			this.front.height = h;
			this.back.height = h;
			return this;
		}
	}

});
sCanvas.fn.extend(true, {

	//stores all canvas objects as map (id: object)
	objects: { },

	//stores bottom canvas object
	bottom: null,

	//stores top canvas object
	top: null,

	//stores default attributes values of newly created objects
	defaultAttrs: {
		x: 0,
		y: 0,
		rotation: 0,
		id: null,
		group: [ ],
		visible: true,
		stroke: null,
		fill: "black",
		originX: 0,
		originY: 0,
		lineWidth: 1,
		lineCap: "butt",
		lineJoin: "miter",
		miterLimit: 10,
		opacity: 1,
		shadowColor: "rgba(0, 0, 0, 0)",
		shadowBlur: 0,
		shadowX: 0,
		shadowY: 0
	},

	//performs all common tasks while creating new object
	register: function(obj) {
		//if registering first object
		if (this.top !== null) {
			this.top.over(obj);
		} else {
			this.top = obj;
			this.bottom = obj;
		}
	},

	//returns canvas object with passed id
	get: function(id) {
		if (this.objects[id] !== undefined) {
			return this.objects[id];
		} else {
			return this.null();
		}
	},

	//adds object with id
	addIdObject: function(obj, id) {
		this.objects[id] = obj;
	},

	//changes objects id
	changeIdObject: function(oldId, newId) {
		this.objects[newId] = this.objects[oldId];
		delete this.objects[oldId];
	},

	//remove object with id
	removeIdObject: function(id) {
		delete this.objects[id];
	}
	
});



var 
	//parent of all canvas objects
	CanvasObject = function() {
		return this;
	};

sCanvas.extend(CanvasObject.prototype, {

	//stores sCanvas reference
	sc: null,

	//stores object attributes
	attrs: null,

	//stores actual relative object origin position in pixels
	origin: null,

	//stores supported attributes
	supportedAttrs: null,

	//stores supported attributes
	animatableAttrs: null,

	//stores handler binded to attribute change
	attrHandlers: null,

	//stores reference to the over object
	overObj: null,

	//stores reference to the under object
	underObj: null,

	//stores animation queue
	animations: null,

	//stores event handlers
	handlers: null,
	
	//stores data associated with object
	storedData: null,

	//initializes object
	init: function(sc) {
		this.sc = sc;

		this.handlers = {
			mousemove: [ ],
			mouseenter: [ ],
			mouseleave: [ ],
			mouseup: [ ],
			mousedown: [ ],
			click: [ ],
			dblclick: [ ]
		};

		this.supportedAttrs = (
			"x y rotation originX originY"
			+ " fill stroke id group"
			+ " lineWidth lineCap lineJoin miterLimit"
			+ " shadowColor shadowBlur shadowX shadowY"
			+ " visible opacity"
		).split(" ");

		this.animatableAttrs = (
			"x y rotation shadowBlur shadowX shadowY opacity lineWidth"
		).split(" ");

		this.attrHandlers = {
			id: this.setId,
			group: this.setGroup,
			originX: this.setOrigin,
			originY: this.setOrigin,
			rotation: this.setRotation
		};

		this.origin = {
			x: 0,
			y: 0
		};

		this.animations = new AnimationQueue();
	},

	//returns true if object is null object
	isNull: function() {
		return (this instanceof CanvasNull);
	},

	//makes object visible
	show: function() {
		if (this.isNull()) {
			return this;
		}

		this.attr("visible", true);

		return this;
	},

	//makes object invisible
	hide: function() {
		if (this.isNull()) {
			return this;
		}

		this.attr("visible", false);

		return this;
	},

	//returns true if object is visible
	isVisible: function() {
		return !!this.attrs.visible;
	},

	//creates path for drawing filled object
	createFillPath: function(cx) {
		throw new Error("Method CanvasObject.createFillPath() should be overriden!");
	},

	//creates path for drawing object stroke
	createStrokePath: function(cx) {
		throw new Error("Method CanvasObject.createStrokePath() should be overriden!");
	},

	//creates path for checking if point is in object
	createPointInPath: function(cx) {
		throw new Error("Method CanvasObject.createPointInPath() should be overriden!");
	},

	//returns object containing info about the most top, right, bottom and left point of object
	box: function() {
		throw new Error("Method CanvasObject.box() should be overriden!");
	},
	
	//returns rotated center poínt
	rotatedCenter: function() {
		var x, y, r, w, a;
		
		//if origin was not moved
		if (this.origin.x === 0 && this.origin.y === 0) {
			return {
				x: this.attrs.x,
				y: this.attrs.y
			};
		} else {
			x = this.origin.x; //x-axis distance
			y = this.origin.y; //y-axis distance
			r = Math.sqrt(x * x + y * y); //absolute distance
			
			w = Math.atan(y / x);
			
			//get angle between rotated r and x-axis
			if (x >= 0) {
				a = Math.PI + w + this.attrs.rotation;
			} else {
				if (y >= 0) {
					a = w + this.attrs.rotation;
				} else {
					a = 2 * Math.PI + w + this.attrs.rotation;
				}
			}
			
			return {
				x: this.attrs.x + r * Math.cos(a),
				y: this.attrs.y + r * Math.sin(a)
			};
		}
	},

	//draws object on canvas
	draw: function(cx) {
		if (this.isVisible()) {
			cx.shadowColor = this.attrs.shadowColor;
			cx.shadowBlur = this.attrs.shadowBlur;
			cx.shadowOffsetX = this.attrs.shadowX;
			cx.shadowOffsetY = this.attrs.shadowY;
			cx.globalAlpha = this.attrs.opacity;
			if (this.attrs.fill !== null) {
				this.createFillPath(cx);
				cx.fillStyle = this.attrs.fill;
				cx.fill();
			}

			if (this.attrs.stroke !== null && this.attrs.lineWidth > 0) {
				this.createStrokePath(cx);
				cx.strokeStyle = this.attrs.stroke;
				cx.lineWidth = this.attrs.lineWidth;
				cx.lineCap = this.attrs.lineCap;
				cx.lineJoin = this.attrs.lineJoin;
				cx.miterLimit = this.attrs.miterLimit;
				cx.stroke();
			}
		}
	},

	//gets/sets over object
	over: function(obj) {
		if (this.isNull()) {
			return this;
		}

		if (obj === undefined) {
			//getter
			return this.overObj;
		} else {
			//setter

			//update old neighbours of passed object and global stack
			if (obj.overObj !== null) {
				obj.overObj.underObj = obj.underObj;
			}
			
			if (obj.underObj !== null) {
				obj.underObj.overObj = obj.overObj;
			}

			//update passed object
			obj.overObj = this.overObj;
			obj.underObj = this;

			//update new neighbours of passed object
			if (this.overObj !== null) {
				this.overObj.underObj = obj;
			} else {
				this.sc.top = obj;
			}
			this.overObj = obj;
			
			this.sc.autorepaint();

			return this;
		}
	},

	//gets/sets under object
	under: function(obj) {
		if (this.isNull()) {
			return this;
		}

		if (obj === undefined) {
			//getter
			return this.underObj;
		} else {
			//setter

			//update old neighbours of passed object
			if (obj.overObj !== null) {
				obj.overObj.underObj = obj.underObj;
			}
			if (obj.underObj !== null) {
				obj.underObj.overObj = obj.overObj;
			}

			//update passed object
			obj.underObj = this.underObj;
			obj.overObj = this;

			//update new neighbours of passed object
			if (this.underObj !== null) {
				this.underObj.overObj = obj;
			} else {
				this.sc.bottom = obj;
			}
			this.underObj = obj;

			this.sc.autorepaint();

			return this;
		}
	},

	//adds supported attribute for object
	addSupportedAttr: function(attr) {
		var i;
		if (typeof attr === "string") {
			this.supportedAttrs.push(attr);
		} else {
			for (i = 0; i < attr.length; i++) {
				this.supportedAttrs.push(attr[i]);
			}
		}
	},

	//adds animatable attribute for object
	addAnimatableAttr: function(attr) {
		var i;
		if (typeof attr === "string") {
			this.animatableAttrs.push(attr);
		} else {
			for (i = 0; i < attr.length; i++) {
				this.animatableAttrs.push(attr[i]);
			}
		}
	},

	//initializes attrs with supported attributes and sets them to default value
	initAttrs: function() {
		var attr, i,
			defaults = this.sc.defaultAttrs;

		this.attrs = { };
		for (i = 0; i < this.supportedAttrs.length; i++) {
			attr = this.supportedAttrs[i];
			if (defaults[attr] !== undefined) {
				this.attrs[attr] = defaults[attr];
			} else {
				this.attrs[attr] = null;
			}
		}
		//TODO: handle this nicely
		this.attrs.group = [ ];
	},

	//gets/sets object attribute(s)
	attr: function(name, value) {
		if (this.isNull()) {
			return this;
		}

		var attr, temp;

		//handling one attribute
		if (typeof name === "string") {
			//check if attribute is supported
			if (!this.attrExists(name)) {
				throw new Error("Attribute " + name + " is not supported.");
			}

			if (value === undefined) {
				//getting value
				return this.attrs[name];
			} else {
				//setting value
				this.setAttr(name, value);
				this.sc.autorepaint();
				return this;
			}
		}

		//setting multiple attributes
		//TODO: check if name is map

		//ensure setting x/originY after setting x/y
//			if (name.originX !== undefined) {
//				temp = name.originX;
//				delete name.originX;
//				name.originX = temp;
//			}
//			if (name.originY !== undefined) {
//				temp = name.originY;
//				delete name.originY;
//				name.originY = temp;
//			}

		for (attr in name) {
			if (!this.attrExists(attr)) {
				throw new Error("Attribute " + attr + " is not supported.");
			} else {
				this.setAttr(attr, name[attr]);
			}
		}
		this.sc.autorepaint();
		return this;
	},

	//checks if attribute is supported
	attrExists: function(name) {
		return this.attrs.hasOwnProperty(name);
	},

	//sets attribute value
	setAttr: function(name, value) {
		//if is set handler for attribut change invoke it
		//otherwise just set attribute value
		if (this.attrHandlers[name] !== undefined) {
			this.attrHandlers[name].apply(this, [value, this.attrs[name], name]);
		} else {
			this.attrs[name] = value;
		}
	},

	//invoked when object id is set
	setId: function(id, oldId) {
		if (id === "" || id === null) { //if id is removed
			if (oldId !== null) {
				this.sc.removeIdObject(oldId);
				this.attrs.id = null;
			}
		} else if (sCanvas.isValidId(id)) { //if id is valid
			if (this.sc.get(id).isNull()) { //if id is not already used
				if (oldId !== null) { //if object already has id
					this.sc.changeIdObject(oldId, id);
				} else {
					this.sc.addIdObject(this, id);
				}
				this.attrs.id = id;
			} else {
				throw new Error("Passed ID is already used.");
			}
		} else {
			throw new Error("Passed ID is not valid.");
		}
	},

	//invoked when object group is set
	setGroup: function(group, oldGroup) {
		var i, groups;

		//remove all current groups
		if (oldGroup !== null) {
			for (i = 0; i < oldGroup.length; i++) {
				this.removeGroup(oldGroup[i]);
			}
		}
		this.attrs.group = [ ];

		if (group === "" || group === null) {
			;
		} else if (typeof group === "string") {
			groups = group.split(" ");

			for (i = 0; i < groups.length; i++) {
				if (!groups[i].match(/^\s*$/)) { //if group is not empty string
					this.addGroup(groups[i]);
				}
			}
		} else {
			//TODO: check if passed object is group
			group.add(this);
		}
	},

	//invoked when object origin attribute is set
	setOrigin: function(value, old, origin) {
		var axis;
		if (origin === "originX") {
			axis = "x";
		} else {
			axis = "y";
		}

		if (typeof value === "number") {
			//this.origin[axis] = this.attrs[axis] - old + value;
			this.origin[axis] = value;
		} else if (typeof value === "string") {
			if (value.charAt(0) === "=") {
				//this.origin[axis] = parseFloat(value.slice(1), 10);
				this.origin[axis] = -this.attrs[axis] + old + parseFloat(value.slice(1), 10);
//				} else if (value.charAt(value.length - 1) === "%") {
//					this.origin[axis] = edge + Math.round(parseFloat(value, 10) * size / 100);
			} else {
				//this.origin[axis] = this.attrs[axis] - old + parseFloat(value, 10);
				this.origin[axis] = parseFloat(value, 10);
			}
		} // else do nothing

		//recount x/y attribute so object stays on position
		this.attrs[axis] = this.attrs[axis] - old + this.origin[axis];

		this.attrs[origin] = value;
	},

	//invoked when object rotation is set
	setRotation: function(value) {
		var val;
		
		if (typeof value === "string") {
			if (value.indexOf("rad") !== -1) {
				val = parseFloat(value, 10);
			} else {
				val = parseFloat(value, 10) * Math.PI / 180;
			}
		} else {
			val = +value * Math.PI / 180;
		}
		
		this.attrs.rotation = val;
	},

	//adds object to group
	addGroup: function(id) {
		this.sc.group(id).add(this);

		return this;
	},

	//removes object from group
	removeGroup: function(id) {
		this.sc.group(id).remove(this);

		return this;
	},

	//returns true if object is in group
	inGroup: function(id) {
		return this.sc.group(id).has(this);
	},

	//adds group to object attribute
	//internal
	group: function(id) {
		if (this.attrs.group === null) {
			this.attrs.group = [ id ];
		} else {
			this.attrs.group.push(id);
		}
	},

	//removes group from object attribute
	//internal
	ungroup: function(id) {
		var i = this.attrs.group.indexOf(id);
		if (i !== -1) {
			this.attrs.group.splice(i, 1);
		}
	},

	//registers object animation
	animate: function(attr, duration, easing, complete) {
		if (this.isNull()) {
			return this;
		}

		var options, anim;

		//unify arguments and make options map
		if (duration === undefined) {
			options = { };
		} else if (typeof duration === "number" || typeof duration === "string") {
			options = {
				duration: duration
			};

			if (typeof easing === "string") {
				options.easing = easing;
			} else if (typeof easing === "function") {
				options.complete = easing;
			}

			if (typeof complete === "function") {
				options.complete = complete;
			}
		} else if (typeof duration === "function") {
			options = {
				complete: duration
			};
		} else {
			options = duration;
		}

		this.addAnimation(new Animation(this, attr, options));

		return this;
	},

	//adds function to animation queue
	queue: function(fn) {
		if (this.isNull()) {
			return this;
		}

		if (typeof fn !== "function") {
			throw new TypeError("Function was not passed.");
		}

		this.addAnimation(new Animation(this, { }, {
			duration: 0,
			complete: fn
		}));

		return this;
	},

	//delay execution of animations in animation queue
	delay: function(duration) {
		if (this.isNull()) {
			return this;
		}

		this.addAnimation(new Animation(this, { }, {
			duration: duration
		}));

		return this;
	},

	//stop currently running animation
	stop: function(clear, jump) {
		if (this.isNull()) {
			return this;
		}

		if (this.animations.isEmpty()) {
			return this;
		}

		var anim = this.animations.peek(),
			time = sCanvas.now();

		if (!!jump) {
			sCanvas.extend(this.attrs, anim.finish());
			if (anim.complete !== null) {
				anim.complete.apply(this, [ ]);
			}
			this.sc.repaint();
		}

		if (!!clear) {
			this.clearQueue();
		}

		if (anim.isRepeated()) {
			anim.repeat(true);
			this.stepAnimation(time);
		} else {
			this.animations.remove();

			if (this.animations.isEmpty()) {
				this.sc.removeAnimatedObject(this);
			} else {
				this.animations.peek().start(time);
				this.stepAnimation(time);
			}
		}

		return this;
	},

	//removes all animations that have not started
	clearQueue: function() {
		if (this.isNull()) {
			return this;
		}

		if (!this.animations.isEmpty()) {
			this.animations.peek().repeats = 1;
			this.animations.removeWaiting();
		}

		return this;
	},

	//immediatelly finishes whole animation queue
	finish: function() {
		if (this.isNull()) {
			return this;
		}

		var anim;

		if (!this.animations.isEmpty()) {
			do {
				anim = this.animations.peek();
				anim.computeAttrs();
				sCanvas.extend(this.attrs, anim.finish());
				if (anim.complete !== null) {
					anim.complete.apply(this, [ ]);
				}
				this.animations.remove();
			} while (!this.animations.isEmpty());
			this.sc.repaint();
			this.sc.removeAnimatedObject(this);
		}

		return this;
	},

	//pauses running animation
	pause: function() {
		if (this.isNull()) {
			return this;
		}

		if (!this.animations.isEmpty()) {
			this.animations.peek().pause();
		}

		return this;
	},

	//reruns paused animation
	play: function() {
		if (this.isNull()) {
			return this;
		}

		if (!this.animations.isEmpty()) {
			this.animations.peek().play();
		}

		return this;
	},

	//returns true if animation is paused
	isPaused: function() {
		if (!this.animations.isEmpty()) {
			return this.animations.peek().isPaused();
		} else {
			return false;
		}
	},

	//changes object attributes during animation
	stepAnimation: function(time) {
		var anim = this.animations.peek();
		sCanvas.extend(this.attrs, anim.next(time));

		//if animation finished, fire next, or if there is no one, tell it to canvas
		if (anim.isFinished()) {
			if (anim.complete !== null) {
				anim.complete.apply(this, [ ]);
			}

			if (anim.isRepeated()) {
				anim.repeat();
				this.stepAnimation(time);
			} else {
				this.animations.remove();

				if (this.animations.isEmpty()) {
					this.sc.removeAnimatedObject(this);
				} else {
					this.animations.peek().start(anim.ending);
					this.stepAnimation(time);
				}
			}
		}
	},

	//pushes animation to animation queue
	addAnimation: function(anim) {
		this.animations.add(anim);

		//if animation queue was empty before adding new animation - fire it
		if (this.animations.length === 1) {
			anim.start();
			this.sc.addAnimatedObject(this);
		}
	},

	//binds event handler
	bind: function(event, fn) {
		if (this.isNull()) {
			return this;
		}

		if (typeof fn !== "function") {
			throw new TypeError("Event handler was not passed.");
		}

		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		if (this.handlers[event].length === 0) {
			this.sc.addEventObject(this, event);
		}

		this.handlers[event].push(fn);

		return this;
	},
	
	//unbinds event handler(s)
	unbind: function(event, fn) {
		if (this.isNull()) {
			return this;
		}

		var i, empty;
		
		if (arguments.length === 0) {
			//removing all handlers of all event types
			for (i in this.handlers) {
				this.handlers[i] = [ ];
				this.sc.removeEventObject(this, i);
			}
		} else {
			if (this.handlers[event] === undefined) {
				throw new ReferenceError("Passed event is not supported.");
			}
			
			if (arguments.length === 1) {
				//removing all handlers of passed event type
				this.handlers[event] = [ ];
				this.sc.removeEventObject(this, event);
			} else {
				//removing only passed handler
				i = this.handlers[event].indexOf(fn);
				if (i !== -1) {
					delete this.handlers[event][i];
					
					//check if this has been last handler
					empty = true;
					for (i in this.handlers[event]) {
						empty = false;
						break;
					}
					
					if (empty) {
						this.handlers[event] = [ ];
						this.sc.removeEventObject(this, event);
					}
				}
			}
		}
		
		return this;
	},

	//triggers event
	trigger: function(event, data) {
		if (this.isNull()) {
			return this;
		}

		var args, e, i;

		//unify arguments
		if (event instanceof EventObject) {
			e = event;
			event = event.type;
		} else {
			e = sCanvas.Event(event);
		}

		//check if passed event type is supported
		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		args = [ e ];
		if (sCanvas.isArray(data)) {
			for (i = 0; i < data.length; i++) {
				args.push(data[i]);
			}
		}

		//execute all binded handlers
		for (i in this.handlers[event]) {
			this.handlers[event][i].apply(this, args);

			//if executing of rest of the handlers was stopped
			if (e.isImmediatePropagationStopped()) {
				break;
			}
		}

		//if remove is set to true, handlers are removed
		if (e.isRemovingHandlers()) {
			this.unbind(event);
		}

		return this;
	},

	//checks if specified point is in object
	isPointIn: function(x, y) {
		var box,
			result = false;
		//TODO: optimize - not to check on every invocation
		if (typeof this.sc.frontCx.isPointInPath === "function") {
			this.createPointInPath(this.sc.frontCx);
			result = this.sc.frontCx.isPointInPath(x, y);
		} else {
			//if native isPointInPath is not supported, use box()
			box = this.box();
			
			if (x >= box.left && x <= box.right && y >= box.top && y <= box.bottom) {
				result = true;
			}
		}
		return result;
	},
	
	//stores data associated with object
	data: function(key, value) {
		if (this.isNull()) {
			return this;
		}
		
		if (arguments.length === 0) {
			//getting all associated data
			
			if (this.storedData === null) {
				return { };
			}
			
			return sCanvas.extend({ }, this.storedData);
		} else if (arguments.length === 1) {
			//getting value
			
			//if no data has been stored yet
			if (this.storedData === null) {
				return undefined;
			}
			
			return this.storedData[key];
		} else {
			//setting value
			if (sCanvas.isValidId(key)) {
				if (this.storedData === null) {
					this.storedData = { };
				}
				this.storedData[key] = value;
			} else {
				throw new Error("Passed key is not valid.");
			}
			
			return this;
		}
	},
	
	//removes data associated with object
	removeData: function(key) {
		if (this.isNull()) {
			return this;
		}
		
		//if no data has been stored yet
		if (this.storedData === null) {
			return this;
		}
		
		if (key === undefined) {
			//removing all data
			this.storedData = null;
		} else {
			//removing data with passed key
			delete this.storedData[key];
			
			//if last piece of data has been removed
			if (sCanvas.isEmpty(this.storedData)) {
				this.storedData = null;
			}
		}
		
		return this;
	},
	
	//returns true if object has associated data with passed key
	//if no key is passed, returns true if any data has been associated
	hasData: function(key) {
		if (this.isNull()) {
			return this;
		}
		
		var result = false;
		
		if (key === undefined) {
			result = this.storedData !== null;
		} else {
			result = this.storedData !== null && this.storedData.hasOwnProperty(key);
		}
		
		return result;
	},
	
	remove: function() {
		if (this.isNull()) {
			return this;
		}
		
		//update neighbours of removed object and global stack
		if (this.overObj !== null) {
			this.overObj.underObj = this.underObj;
		} else {
			this.sc.top = this.underObj;
		}
		
		if (this.underObj !== null) {
			this.underObj.overObj = this.overObj;
		} else {
			this.sc.bottom = this.overObj;
		}
		
		this.overObj = null;
		this.underObj = null;
		this.attr({
			id: null,
			group: null
		})
		.stop(true)
		.unbind();
		
		//this.sc.repaint();
	}

});

//add event shortcuts
var events = (
	"click dblclick mouseup mousedown"
	+ " mouseenter mousemove mouseleave"
).split(" ");

for (var ei = 0; ei < events.length; ei++) {
	CanvasObject.prototype[events[ei]] = (function() {
		var type = events[ei];
		return function(fn) {
			return fn === undefined ? this.trigger(type) : this.bind(type, fn);
		};
	}());
}

CanvasObject.prototype.hover = function(fnIn, fnOut) {
	return this.bind("mouseenter", fnIn).bind("mouseleave", fnOut || fnIn);
};

var
	//stores info about animation and about its progress
	Animation = function(obj, attr, options) {
		var name;

		//check if attributes are animatable
		for (name in attr) {
			if (obj.animatableAttrs.indexOf(name) === -1) {
				throw new Error("Attribute " + name + " is not animatable.");
			}
		}

		this.obj = obj;
		this.modification = attr;
		this.duration = Animation.getDuration(options.duration);

		if (typeof options.complete === "function") {
			this.complete = options.complete;
		}

		if (typeof options.repeat === "number" || typeof options.repeat === "string") {
			this.repeats = +options.repeat;
		}
	};

//returns duration in milliseconds
Animation.getDuration = function(duration) {
	if (typeof duration === "number") {
		return duration;
	} else if (duration === "slow") {
		return 600;
	} else if (duration === "fast") {
		return 200;
	} else if (typeof duration === "string") {
		return parseFloat(duration, 10);
	} else {
		return 400;
	}
};

Animation.prototype = {

	//stores reference to animated object
	obj: null,

	//reference to next animation
	after: null,

	//stores
	modification: null,

	//stores initial state of attributes at the animation beginning
	startAttrs: null,

	//stores desired state of attributes at the animation end
	endAttrs: null,

	//stores duration of animation
	duration: -1,

	//stores time when animation was started
	started: -1,

	//stores time when animation should finish
	ending: -1,

	//stores if animation already finished
	finished: false,

	//stores function to be executed when animation finishes
	complete: null,

	//stores how many times should be animation repeated
	repeats: 1,

	//stores total duration of pauses
	pauseDuration: 0,

	//stores when current pause was started
	pauseStarted: -1,

	//starts animation
	start: function(time) {
		if (time === undefined) {
			time = sCanvas.now();
		}

		this.started = time;
		this.ending = this.started + this.duration;

		this.computeAttrs();
	},

	//sets start and end attribute values
	computeAttrs: function() {
		var name, value, computedValue;

		//if attrs are already computed, do not compute them again
		if (this.startAttrs !== null) {
			return;
		}

		this.startAttrs = { };
		this.endAttrs = { };

		for (name in this.modification) {
			this.startAttrs[name] = this.obj.attr(name);

			value = this.modification[name];

			if (typeof value === "string") {
				if (value.charAt(1) === "=") {
					if (value.charAt(0) === "+") {
						this.endAttrs[name] = this.startAttrs[name] + parseFloat(value.slice(2), 10);
					} else if (value.charAt(0) === "-") {
						this.endAttrs[name] = this.startAttrs[name] - parseFloat(value.slice(2), 10);
					} else {
						this.endAttrs[name] = this.startAttrs[name];
					}
				} else {
					this.endAttrs[name] = parseFloat(value, 10);
				}
			} else if (typeof value === "number") {
				this.endAttrs[name] = value;
			} else if (typeof value === "function") {
				computedValue = value.apply(this.obj, [ this.startAttrs[name] ]);
				if (typeof computedValue === "number") {
					this.endAttrs[name] = computedValue;
				} else {
					this.endAttrs[name] = this.startAttrs[name];
				}
			} else {
				this.endAttrs[name] = this.startAttrs[name];
			}
		}
	},

	//counts object attributes for next animation frame
	next: function(time) {
		var name, result;

		//if animation is currently pause, adjust time
		if (this.isPaused()) {
			time = this.pauseStarted;
		}

		if (time > this.ending) {
			return this.finish();
		} else {
			result = { };
			for (name in this.endAttrs) {
				result[name] = this.startAttrs[name] + (this.endAttrs[name] - this.startAttrs[name]) * ((time - this.started - this.pauseDuration) / this.duration);
			}
			return result;
		}
	},

	//immediatelly finishes animation
	finish: function() {
		this.finished = true;
		return this.endAttrs;
	},

	//returns true if animation already finished
	isFinished: function() {
		return this.finished;
	},

	//returns true if animations repeats
	isRepeated: function() {
		return this.repeats > 1;
	},

	//restarts animation and lowers animation repetition
	repeat: function(stopped) {
		if (this.isRepeated()) {
			this.finished = false;
			this.pauseDuration = 0;
			this.pauseStarted = -1;
			this.start(stopped === true ? undefined : this.ending);
			this.repeats--;
		}
	},

	//pauses animation
	pause: function() {
		if (!this.isPaused()) {
			this.pauseStarted = sCanvas.now();
		}
	},

	//ends pause
	play: function() {
		var p;
		if (this.isPaused()) {
			p = sCanvas.now() - this.pauseStarted;
			this.pauseDuration += p;
			this.ending += p;
			this.pauseStarted = -1;
		}
	},

	//returns true if animation is paused
	isPaused: function() {
		return this.pauseStarted !== -1;
	}

};




var
	//stores info about animation queue, fires animations
	AnimationQueue = function() {

	};

AnimationQueue.prototype = {

	//stores first animation in queue
	first: null,

	//stores last animation in queue
	last: null,

	//stores count of animations in queue
	length: 0,

	//returns true, if there is no animation in queue
	isEmpty: function() {
		return this.length === 0;
	},

	//adds animation to queue
	add: function(animation) {
		if (!this.isEmpty()) {
			this.last.after = animation;
		} else {
			//queue is empty, so added animation is also first
			this.first = animation;
		}
		this.last = animation;

		this.length++;
	},

	//removes first animation from queue
	remove: function(animation) {
		if (this.isEmpty()) { //if queue is empty, just return null
			return null;
		}

		var result = this.first;

		if (this.length === 1) {
			this.last = null;
			this.first = null;
		} else {
			this.first = this.first.after;
		}

		this.length--;

		return result;
	},

	//returns first animation in queue
	peek: function() {
		return this.first;
	},

	//returns queue as array
	get: function() {
		var anim = this.first,
			result = [ ];

		while (anim !== null) {
			result.push(anim);
			anim = anim.after;
		}

		return result;
	},

	//removes all animations except the first one
	removeWaiting: function() {
		if (this.length > 1) {
			this.last = this.first;
			this.length = 1;
			this.first.after = null;
		}
	},

	//removes all animations
	clear: function() {
		this.last = null;
		this.first = null;
		this.length = 0;
	}

};




sCanvas.fn.extend(true, {
	
	//stores currently animated objects
	animated: [ ],

	//count of currently animated objects
	animatedCount: 0,

	//true if canvas is currently performing an animation
	isAnimating: false,

	//registers animated object
	addAnimatedObject: function(obj) {
		this.animated.push(obj);
		this.animatedCount++;
		//TODO: handle adding already added object
		//if animation is not running, run it
		if (!this.isAnimating) {
			this.isAnimating = true;
			this.runAnimation();
		}
	},

	//unregisters animated object
	removeAnimatedObject: function(obj) {
		delete this.animated[this.animated.indexOf(obj)];
		this.animatedCount--;
		//TODO: handle removing object which was not added
		//if there is no animated object, stop animating
		if (this.animatedCount === 0) {
			this.isAnimating = false;
			this.animated = [ ];
		}
	},

	//performs animation frame by frame
	runAnimation: function() {
		var i,
			sc = this,
			time = sCanvas.now();

		//prevents requesting next frame, if there is no animated object
		//also prevents needless repainting - since we are requesting
		//frame and then we count if it is needed at all
		//so there is always one "blank" requested frame at the end
		//of animation when nothing is done
		//TODO: think if this can be fixed
		if (this.isAnimating) {
			window.requestAnimationFrame(function() {
				sc.runAnimation();
			});

			//update all animated objects attributes
			for (i in this.animated) {
				this.animated[i].stepAnimation(time);
			}
			this.repaint();
		}
	}

});



//creates rect object
sCanvas.fn.circ = function() {
	if (arguments.length !== 1 && arguments.length !== 3) {
		throw new Error("Wrong arguments was passed to circ().");
	}
	
	var attrs,
		obj = new CanvasCirc(this);
	
	if (arguments.length === 1) {
		attrs = arguments[0];
	} else {
		attrs = {
			x: arguments[0],
			y: arguments[1],
			radius: arguments[2]
		};
	}

	obj.attr(attrs);

	this.register(obj);
	this.autorepaint();
	return obj;
};



var CanvasCirc = function(sc) {
	this.init(sc);
	this.addSupportedAttr("radius");
	this.addAnimatableAttr("radius");
	this.initAttrs();
	return this;
};

CanvasCirc.prototype = new CanvasObject();

CanvasCirc.prototype.createFillPath = function(cx) {
	cx.save();
	cx.translate(this.attrs.x, this.attrs.y);
	cx.rotate(this.attrs.rotation);
	cx.beginPath();
	cx.arc(
		-this.origin.x,
		-this.origin.y,
		this.attrs.radius,
		0,
		2 * Math.PI,
		false);
	cx.closePath();
	cx.restore();
};

CanvasCirc.prototype.createStrokePath = function(cx) {
	this.createFillPath(cx);
};

CanvasCirc.prototype.createPointInPath = function(cx) {
	this.createFillPath(cx);
};

CanvasCirc.prototype.box = function() {
	var center;
	if (this.attrs.rotation === 0) {
		return {
			top: this.attrs.y - this.origin.y - this.attrs.radius,
			right: this.attrs.x - this.origin.x + this.attrs.radius,
			bottom: this.attrs.y - this.origin.y + this.attrs.radius,
			left: this.attrs.x - this.origin.x - this.attrs.radius,
			width: 2 * this.attrs.radius,
			height: 2 * this.attrs.radius
		};
	} else {
		center = this.rotatedCenter();
		return {
			top: Math.round(center.y - this.attrs.radius),
			right: Math.round(center.x + this.attrs.radius),
			bottom: Math.round(center.y + this.attrs.radius),
			left: Math.round(center.x - this.attrs.radius),
			width: 2 * this.attrs.radius,
			height: 2 * this.attrs.radius
		};
	}
};



sCanvas.fn.extend(true, {
	
	//stores object with registered events
	events: {
		mousemove: [ ],
		mouseenter: [ ],
		mouseleave: [ ],
		mouseup: [ ],
		mousedown: [ ],
		click: [ ],
		dblclick: [ ]
	},

	//stores last coordinates during mousemove
	lastMovePoint: null,

	//registers object with event
	addEventObject: function(obj, event) {
		var sc = this;
		
		//TODO: handle already added object
		this.events[event].push(obj);

		if (event === "click" || event === "dblclick" || event === "mouseup" || event === "mousedown") {
			// < 2 becouse object has been already pushed few lines above
			if (this.events[event].length < 2) {
				this.front["on" + event] = function(e) {
					sc.checkClickEvent(e, event);
				};
			}
		} else if (event === "mousemove" || event === "mouseleave" || event === "mouseenter") {
			// < 2 becouse object has been already pushed few lines above
			if (this.events["mousemove"].length + this.events["mouseleave"].length + this.events["mouseenter"].length < 2) {
				this.front["onmousemove"] = function(e) {
					sc.checkMoveEvent(e);
				};
			}
		}
	},

	//unregisters object with event 
	removeEventObject: function(obj, event) {
		var i = this.events[event].indexOf(obj);
		if (i !== -1) {
			delete this.events[event][i];
			
			if (event === "click" || event === "dblclick" || event === "mouseup" || event === "mousedown") {
				if (sCanvas.isEmpty(this.events[event])) {
					this.front["on" + event] = null;
					this.events[event] = [ ];
				}
			} else if (event === "mousemove" || event === "mouseleave" || event === "mouseenter") {
				if (sCanvas.isEmpty(this.events["mousemove"]) && sCanvas.isEmpty(this.events["mouseleave"]) && sCanvas.isEmpty(this.events["mouseenter"])) {
					this.front["onmousemove"] = null;
					this.events["mousemove"] = [ ];
					this.events["mouseleave"] = [ ];
					this.events["mouseenter"] = [ ];
				}
			}
		}
	},

	//if click was performed over object, triggers its event
	checkClickEvent: function(e, event) {
		var i, obj,
			evt = this.getBrowserEventData(e);
		
		evt["canvasX"] = e.pageX - this.pos.left;
		evt["canvasY"] = e.pageY - this.pos.top;
		
		for (i in this.events[event]) {
			var obj = this.events[event][i];
			if (obj.isPointIn(evt["canvasX"], evt["canvasY"])) {
				obj.trigger(
					sCanvas.Event(event, evt)
				);
			}
		}
	},

	//if move was performed over object, triggers its event
	checkMoveEvent: function(e) {
		var i, obj,
			evt = this.getBrowserEventData(e);
		
		evt["canvasX"] = e.pageX - this.pos.left;
		evt["canvasY"] = e.pageY - this.pos.top;

		//TODO: optimize this
		if (this.events["mouseenter"].length > 0) {
			for (i in this.events["mouseenter"]) {
				var obj = this.events["mouseenter"][i];
				if (obj.isPointIn(evt["canvasX"], evt["canvasY"])
						&& (this.lastMovePoint === null || !obj.isPointIn(this.lastMovePoint.x, this.lastMovePoint.y))) {
					obj.trigger(
						sCanvas.Event("mouseenter", evt)
					);
				}
			}
		}

		if (this.events["mousemove"].length > 0) {
			for (i in this.events["mousemove"]) {
				var obj = this.events["mousemove"][i];
				if (obj.isPointIn(evt["canvasX"], evt["canvasY"])) {
					obj.trigger(
						sCanvas.Event("mousemove", evt)
					);
				}
			}
		}

		if (this.events["mouseleave"].length > 0) {
			for (i in this.events["mouseleave"]) {
				var obj = this.events["mouseleave"][i];
				if (!obj.isPointIn(evt["canvasX"], evt["canvasY"])
						&& (this.lastMovePoint !== null && obj.isPointIn(this.lastMovePoint.x, this.lastMovePoint.y))) {
					obj.trigger(
						sCanvas.Event("mouseleave", evt)
					);
				}
			}
		}

		this.lastMovePoint = { x: evt["canvasX"], y: evt["canvasY"]	};
	},
	
	//copies and returns some data from original browser object
	getBrowserEventData: function(e) {
		var i,
			result = { },
			data = (
				"pageX pageY screenX screenY altKey ctrlKey metaKey shiftKey view"
			).split(" ");
		
		result.timeStamp = sCanvas.now();
		
		for (i = 0; i < data.length; i++) {
			if (e[data[i]] !== undefined) {
				result[data[i]] = e[data[i]];
			}
		}
		
		return result;
	}

});



//object containing event data, it allows to manipulate event behaviour
var EventObject = function(type, data) {
	this.type = type;
	
	if (data !== undefined) {
		sCanvas.extend(this, data);
	}
	
	return this;
};

//stores event type
EventObject.prototype.type = "";

//if true prevents executing next event handler
EventObject.prototype.stopImmediateProp = false;

//if true prevents executing next event handler
EventObject.prototype.remove = false;

//prevents next event handlers from being executed
EventObject.prototype.stopImmediatePropagation = function() {
	this.stopImmediateProp = true;
	return this;
};

//returns true if executing next event handlers was stopped
EventObject.prototype.isImmediatePropagationStopped = function() {
	return this.stopImmediateProp;
};

//if called, handlers will be removed after their triggering
EventObject.prototype.removeHandlers = function() {
	this.remove = true;
	return this;
};

EventObject.prototype.isRemovingHandlers = function() {
	return this.remove;
};



//returns group object with passed id
//if there is no group with this id, new one is created
sCanvas.fn.group = function(id) {
	var obj = this.get(id);

	if (obj.isNull()) {
		if (sCanvas.isValidId(id)) {
			obj = new CanvasGroup(this, id);
			this.addIdObject(obj, id);
		} else {
			throw new Error("Passed ID is not valid.");
		}
	} else if (!sCanvas.isGroup(obj)) {
		throw new Error("Object identified by passed ID is not a group.");
	}

	return obj;
};



var 
	//parent of all canvas objects
	CanvasGroup = function(sc, id) {
		this.sc = sc;
		this.objects = [ ];
		this.id = id;
		return this;
	};

CanvasGroup.prototype = {

	//stores sCanvas reference
	sc: null,

	//stores count of grouped objects
	length: 0,

	//stores references to grouped objects
	objects: null,

	//stores group id
	id: null,

	//adds object to the group
	add: function(obj) {
		//only non-null and non-group object is added and only if it is not already in group
		if (!obj.isNull() && !sCanvas.isGroup(obj) && !this.has(obj)) {
			this.objects.push(obj);
			this.length = this.objects.length;
			obj.group(this.id);
		}
		return this;
	},

	//if called with no argument or false, group is destroyed, but objects live on
	//if called with true, group and all grouped objects are destroyed
	//if called with object, this object is removed from group
	//if object is wanted to be removed, call remove on this object directly
	//if called with integer, object at this position is removed
	//if object at position is wanted to be removed, call group.get(i).remove()
	remove: function(obj) {
		if (obj === undefined) {
			obj = false;
		}

		var i;

		if (typeof obj === "boolean") {
			if (obj) {
				for (i = this.objects.length - 1; i >= 0; i--) {
					this.objects[i].remove();
				}
			} else {
				for (i = this.objects.length - 1; i >= 0; i--) {
					this.objects[i].ungroup(this.id);
				}
			}
			this.sc.removeIdObject(this.id);

			return this.sc.null();
		} else if (typeof obj === "number") {
			i = Math.floor(obj);
		} else {
			i = this.objects.indexOf(obj);
		}

		if (i >= 0 && i < this.objects.length) {
			this.objects[i].ungroup(this.id);
			this.objects.splice(i, 1);
		}

		this.length = this.objects.length;

		return this;
	},

	//returns true if group contains passed object
	has: function(obj) {
		return this.objects.indexOf(obj) !== -1;
	},

	//returns object at i position
	get: function(i) {
		if (this.objects[i] !== undefined) {
			return this.objects[i];
		} else {
			return this.sc.null();
		}
	},

	//calls passed function on all grouped objects
	each: function(fn) {
		var i, ret;

		if (typeof fn !== "function") {
			throw new TypeError("Function was not passed.");
		}

		for (i in this.objects) {
			ret = fn.apply(this.objects[i], [ Number(i) ]);
			if (ret === false) {
				break;
			}
		}
	},

	//returns true if group is empty
	isEmpty: function() {
		return this.length === 0;
	},

	//returns false as group is never considered null
	//TODO: think about returning false if group is empty, true otherwise
	isNull: function() {
		return false;
	},

	//makes object visible
	show: function() {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].show();
		}

		return this;
	},

	//makes object invisible
	hide: function() {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].hide();
		}

		return this;
	},

	//returns true if all objects are visible
	isVisible: function() {
		var i, result = true;

		if (this.length === 0) {
			result = false;
		} else {
			for (i in this.objects) {
				if (!this.objects[i].isVisible()) {
					result = false;
					break;
				}
			}
		}

		return result;
	},

	//draws object on canvas
//		draw: function(cx) {
//			this.createStrokePath(cx);
//			cx.stroke();
//			
//			this.createFillPath(cx);
//			cx.fill();
//		},

	//returns the most top point of grouped objects
//		top: function() {
//			//TODO: implement CanvasGroup.top()
//			return 0;
//		},
//		
//		//returns the most left point of grouped objects
//		left: function() {
//			//TODO: implement CanvasGroup.left()
//			return 0;
//		},
//		
//		//returns the width from most left to most right point of grouped objects
//		width: function() {
//			//TODO: implement CanvasGroup.width()
//			return 0;
//		},
//		
//		//returns the height from most top to most bottom point of grouped objects
//		height: function() {
//			//TODO: implement CanvasGroup.height()
//			return 0;
//		},

	//gets/sets over object
	over: function(obj) {
		if (this.isNull()) {
			return this;
		}
		return this;
		//TODO: think of calling over() of most top grouped object
//			if (obj === undefined) {
//				//getter
//				return this.overObj;
//			} else {
//				//setter
//				
//				//update old neighbours of passed object
//				if (obj.overObj !== null) {
//					obj.overObj.underObj = obj.underObj;
//				}
//				if (obj.underObj !== null) {
//					obj.underObj.overObj = obj.overObj;
//				}
//				
//				//update passed object
//				obj.overObj = this.overObj;
//				obj.underObj = this;
//				
//				////update new neighbours of passed object
//				if (this.overObj !== null) {
//					this.overObj.underObj = obj;
//				}
//				this.overObj = obj;
//			}
	},

	//gets/sets under object
	under: function(obj) {
		if (this.isNull()) {
			return this;
		}
		return this;
		//TODO: think of calling under() of most bottom grouped object
//			if (obj === undefined) {
//				//getter
//				return this.underObj;
//			} else {
//				//setter
//				
//				//update old neighbours of passed object
//				if (obj.overObj !== null) {
//					obj.overObj.underObj = obj.underObj;
//				}
//				if (obj.underObj !== null) {
//					obj.underObj.overObj = obj.overObj;
//				}
//				
//				//update passed object
//				obj.underObj = this.underObj;
//				obj.overObj = this;
//				
//				////update new neighbours of passed object
//				if (this.underObj !== null) {
//					this.underObj.overObj = obj;
//				}
//				this.underObj = obj;
//			}
	},

	//returns attribute value of first grouped objects or
	//sets attribute(s) value of all grouped objects
	attr: function(name, value) {
		if (this.isNull()) {
			return this;
		}

		var i, result = this;

		if (typeof name === "string" && value === undefined) {
			//getting value
			try {
				result = this.get(0).attr(name);
			} catch (e) {
				result = undefined;
			}
		} else {
			//setting value(s)
			try {
				for (i in this.objects) {
					this.objects[i].attr(name, value);
				}
			} catch (e) {}
		}

		return result;
	},

	//registers object animation
	animate: function(attr, duration) {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].animate(attr, duration);
		}
		return this;
	},
	
	pause: function() {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].pause();
		}
		return this;
	},

	play: function() {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].play();
		}
		return this;
	},

	//registers click event handler or triggers click event
	click: function(fn) {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].click(fn);
		}
		return this;
	},

	//binds event handler
	bind: function(event, fn) {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].bind(event, fn);
		}
		return this;
	},

	//triggers event
	trigger: function(event, data) {
		if (this.isNull()) {
			return this;
		}

		var i;
		for (i in this.objects) {
			this.objects[i].trigger(event, data);
		}
		return this;
	}

};




//creates image object
sCanvas.fn.image = function() {
	if (arguments.length !== 2 && arguments.length !== 3) {
		throw new Error("Wrong arguments was passed to image().");
	}
	
	try {
		var attrs,
			obj = new CanvasImage(this, arguments[0]);

		if (arguments.length === 2) {
			attrs = arguments[1];
		} else {
			attrs = {
				x: arguments[1],
				y: arguments[2]
			};
			obj.shorthand = true;
		}

		obj.attr(attrs);

		this.register(obj);
//		this.autorepaint();
		return obj;
	} catch (err) {
		throw(err);
	}
};



var CanvasImage = function(sc, source) {
	var that = this;
	this.init(sc);
	this.addSupportedAttr(["width", "height"/*, "wait"*/]);
	this.addAnimatableAttr(["width", "height"]);
	this.initAttrs();

	this.source = source;

	this.img = new Image();

	if (source instanceof Image) {
		this.img.src = source.src;
	} else if (typeof source === "string") {
		this.img.src = source;
	} else {
		throw new TypeError("Bad image source.");
	}

	this.img.onload = function() {
		that.loaded = true;

		if (that.attrs.width === null) { //if width was not set to resize image
			that.attrs.width = this.width;
		}
		if (that.attrs.height === null) { //if height was not set to resize image
			that.attrs.height = this.height;
		}
		
		if (that.shorthand) {
			that.attrs.x += that.attrs.width / 2;
			that.attrs.y += that.attrs.height / 2;
			delete that.shorthand;
		}

		that.sc.autorepaint();
	};

	return this;
};

CanvasImage.prototype = new CanvasObject();

CanvasImage.prototype.createFillPath = function(cx) {};

CanvasImage.prototype.createStrokePath = function(cx) {};

CanvasImage.prototype.createPointInPath = function(cx) {
	var center = this.center();
	
	cx.save();
	cx.translate(this.attrs.x, this.attrs.y);
	cx.rotate(this.attrs.rotation);
	cx.beginPath();
	cx.moveTo(
		-center.x - this.origin.x,
		-center.y - this.origin.y
	);
	cx.lineTo(
		-center.x - this.origin.x + this.attrs.width,
		-center.y - this.origin.y
	);
	cx.lineTo(
		-center.x - this.origin.x + this.attrs.width,
		-center.y - this.origin.y + this.attrs.height
	);
	cx.lineTo(
		-center.x - this.origin.x,
		-center.y - this.origin.y + this.attrs.height
	);
	cx.lineTo(
		-center.x - this.origin.x,
		-center.y - this.origin.y
	);
	cx.closePath();
	cx.restore();
};

CanvasImage.prototype.draw = function(cx) {
	var center;
	if (this.isVisible() && this.loaded) {
		center = this.center();
	
		//modificators ensures that image will not be blurred
		var modX = this.attrs.width % 2 === 1 ? 0.5 : 0;
		var modY = this.attrs.height % 2 === 1 ? 0.5 : 0;
		
		cx.shadowColor = this.attrs.shadowColor;
		cx.shadowBlur = this.attrs.shadowBlur;
		cx.shadowOffsetX = this.attrs.shadowX;
		cx.shadowOffsetY = this.attrs.shadowY;
		cx.globalAlpha = this.attrs.opacity;
		
		cx.save();
		cx.translate(this.attrs.x, this.attrs.y);
		cx.rotate(this.attrs.rotation);
		cx.drawImage(
			this.img,
			-center.x - this.origin.x + modX,
			-center.y - this.origin.y + modY,
			this.attrs.width,
			this.attrs.height
		);
		cx.restore();
	}
};

//returns center point as relative coordinates from base top left of object
CanvasImage.prototype.center = function() {
	return {
		x: this.attrs.width / 2,
		y: this.attrs.height / 2
	};
},
	
//stores passed image source
CanvasImage.prototype.source = null;

//stores HTMLImage object created from source
CanvasImage.prototype.img = null;

//returns true if image is loaded
CanvasImage.prototype.loaded = false;

CanvasImage.prototype.box = function() {
	var center, width, height, sinRot, sinRot2;
	if (this.attrs.rotation === 0) {
		center = this.center();
		return {
			top: this.attrs.y - center.y - this.origin.y,
			right: this.attrs.x - center.x - this.origin.x + this.attrs.width,
			bottom: this.attrs.y - center.y - this.origin.y + this.attrs.height,
			left: this.attrs.x - center.x - this.origin.x,
			width: this.attrs.width,
			height: this.attrs.height
		};
	} else {
		center = this.rotatedCenter();
		sinRot = Math.sin(this.attrs.rotation);
		sinRot2 = Math.sin(Math.PI - this.attrs.rotation);
		width = Math.abs(this.attrs.height * sinRot + this.attrs.width * sinRot2);
		height = Math.abs(this.attrs.width * sinRot + this.attrs.height * sinRot2);
		
		return {
			top: Math.round(center.y - height / 2),
			right: Math.round(center.x + width / 2),
			bottom: Math.round(center.y + height / 2),
			left: Math.round(center.x - width / 2),
			width: width,
			height: height
		};
	}
};



//creates line object
sCanvas.fn.line = function() {
	if (arguments.length !== 1 && arguments.length !== 2) {
		throw new Error("Wrong arguments was passed to line().");
	}
	
	var attrs,
		obj = new CanvasLine(this);
	
		if (arguments.length === 1) {
			attrs = arguments[0];
		} else {
			attrs = {
				x: arguments[0],
				y: arguments[1]
			};
		}

	obj.attr({
		fill: null,
		stroke: "black"
	});

	obj.attr(attrs);

	this.register(obj);
	this.autorepaint();
	return obj;
};



var CanvasLine = function(sc) {
	this.init(sc);
	this.initAttrs();
	
	this.segments = [ ];
	
	return this;
};

CanvasLine.prototype = new CanvasObject();

CanvasLine.prototype.createFillPath = function(cx) {
	this.createStrokePath(cx);
};

CanvasLine.prototype.createStrokePath = function(cx) {
	//modificators ensures that line will not be blurred
	var modX = (this.attrs.lineWidth) % 2 === 1 ? 0.5 : 0;
	var modY = (this.attrs.lineWidth) % 2 === 1 ? 0.5 : 0;
	
	var i;
	
	cx.save();
	cx.translate(this.attrs.x + modX, this.attrs.y + modY);
	cx.rotate(this.attrs.rotation);
	cx.beginPath();
	
	cx.moveTo(
		-this.origin.x,
		-this.origin.y
	);
		
	for (i = 0; i < this.segments.length; i++) {
		this.segments[i].createPath(cx);
	}
	
	cx.restore();
};

CanvasLine.prototype.createPointInPath = function(cx) {
	this.createFillPath(cx);
};

CanvasLine.prototype.box = function() {};

//stores line segments
CanvasLine.prototype.segments = null;

//adds newly created straight line segment to line
CanvasLine.prototype.addLine = function(x, y) {
	this.segments.push(new CanvasStraightSegment(
		this,
		x - this.attrs.x,
		y - this.attrs.y
	));
	
	this.sc.autorepaint();
	
	return this;
};

//adds newly created quadratic Bezier curve segment to line
CanvasLine.prototype.addQuadratic = function(x, y, cpx, cpy) {
	this.segments.push(new CanvasQuadraticSegment(
		this,
		x - this.attrs.x,
		y - this.attrs.y,
		cpx - this.attrs.x,
		cpy - this.attrs.y
	));
	
	this.sc.autorepaint();
	
	return this;
};

//adds newly created cubic Bezier curve segment to line
CanvasLine.prototype.addCubic = function(x, y, cp1x, cp1y, cp2x, cp2y) {
	this.segments.push(new CanvasCubicSegment(
		this,
		x - this.attrs.x,
		y - this.attrs.y,
		cp1x - this.attrs.x,
		cp1y - this.attrs.y,
		cp2x - this.attrs.x,
		cp2y - this.attrs.y
	));
	
	this.sc.autorepaint();
	
	return this;
};



//parent of all line segments
var CanvasLineSegment = function() {
	return this;
};

//stores reference to line in which segment is placed
CanvasLineSegment.prototype.line = null;

//draws line segment
CanvasLineSegment.prototype.createPath = function(cx) {
	throw new Error("Method CanvasLineSegment.createPath() should be overriden!");
};



//straight line segment
var CanvasStraightSegment = function(line, x, y) {
	this.line = line;
	this.end = {
		x: x,
		y: y
	};
};

CanvasStraightSegment.prototype = new CanvasLineSegment();

CanvasStraightSegment.prototype.createPath = function(cx) {
	cx.lineTo(
		this.end.x,
		this.end.y
	);
};

//end point
CanvasStraightSegment.prototype.end = null;



//quadratic Bezier curve segment
var CanvasQuadraticSegment = function(line, x, y, cpx, cpy) {
	this.line = line;
	this.end = {
		x: x,
		y: y
	};
	this.cp = {
		x: cpx,
		y: cpy
	};
};

CanvasQuadraticSegment.prototype = new CanvasLineSegment();

//end point
CanvasQuadraticSegment.prototype.end = null;

//control point
CanvasQuadraticSegment.prototype.cp = null;

CanvasQuadraticSegment.prototype.createPath = function(cx) {
	cx.quadraticCurveTo(
		this.cp.x,
		this.cp.y,
		this.end.x,
		this.end.y
	);
};



//quadratic Bezier curve segment
var CanvasCubicSegment = function(line, x, y, cp1x, cp1y, cp2x, cp2y) {
	this.line = line;
	this.end = {
		x: x,
		y: y
	};
	this.cp1 = {
		x: cp1x,
		y: cp1y
	};
	this.cp2 = {
		x: cp2x,
		y: cp2y
	};
};

CanvasCubicSegment.prototype = new CanvasLineSegment();

CanvasCubicSegment.prototype.createPath = function(cx) {
	cx.bezierCurveTo(
		this.cp1.x,
		this.cp1.y,
		this.cp2.x,
		this.cp2.y,
		this.end.x,
		this.end.y
	);
};

//end point
CanvasCubicSegment.prototype.end = null;

//first control point
CanvasCubicSegment.prototype.cp1 = null;

//second control point
CanvasCubicSegment.prototype.cp2 = null;



//returns empty element
//usefull on edge situations, where is expected returning object that
//was not found and throwing error is not desired
sCanvas.fn.null = function() {
	return new CanvasNull(this);
};



var CanvasNull = function(sc) {
	this.init(sc);
	this.initAttrs();
	return this;
};

CanvasNull.prototype = new CanvasObject();

CanvasNull.prototype.createFillPath = function(cx) {};

CanvasNull.prototype.createStrokePath = function(cx) {};



//creates rect object
//left, top, width, height
sCanvas.fn.rect = function() {
	if (arguments.length !== 1 && arguments.length !== 4) {
		throw new Error("Wrong arguments was passed to rect().");
	}
	
	var attrs,
		obj = new CanvasRect(this);
	
	if (arguments.length === 1) {
		attrs = arguments[0];
	} else {
		attrs = {
			x: arguments[0] + arguments[2] / 2,
			y: arguments[1] + arguments[3] / 2,
			width: arguments[2],
			height: arguments[3],
		};
	}

	obj.attr(attrs);

	this.register(obj);
	this.autorepaint();
	return obj;
};



var CanvasRect = function(sc) {
	this.init(sc);
	this.addSupportedAttr(["width", "height"]);
	this.addAnimatableAttr(["width", "height"]);
	this.initAttrs();
	return this;
};

CanvasRect.prototype = new CanvasObject();

CanvasRect.prototype.createFillPath = function(cx) {
	var center = this.center();
	
	cx.save();
	cx.translate(this.attrs.x, this.attrs.y);
	cx.rotate(this.attrs.rotation);
	cx.beginPath();
	cx.moveTo(
		-center.x - this.origin.x,
		-center.y - this.origin.y
	);
	cx.lineTo(
		-center.x - this.origin.x + this.attrs.width,
		-center.y - this.origin.y
	);
	cx.lineTo(
		-center.x - this.origin.x + this.attrs.width,
		-center.y - this.origin.y + this.attrs.height
	);
	cx.lineTo(
		-center.x - this.origin.x,
		-center.y - this.origin.y + this.attrs.height
	);
	cx.lineTo(
		-center.x - this.origin.x,
		-center.y - this.origin.y
	);
	cx.closePath();
	cx.restore();
};

CanvasRect.prototype.createStrokePath = function(cx) {
	var center = this.center();
	
	//modificators ensures that rect will not be blurred
	var modX = (this.attrs.lineWidth) % 2 === 1 ? 0.5 : 0;
	var modY = (this.attrs.lineWidth) % 2 === 1 ? 0.5 : 0;
	
	cx.save();
	cx.translate(this.attrs.x, this.attrs.y);
	cx.rotate(this.attrs.rotation);
	cx.beginPath();
	cx.moveTo(
		-center.x - this.origin.x + modX,
		-center.y - this.origin.y + modY
	);
	cx.lineTo(
		-center.x - this.origin.x + this.attrs.width + modX,
		-center.y - this.origin.y + modY
	);
	cx.lineTo(
		-center.x - this.origin.x + this.attrs.width + modX,
		-center.y - this.origin.y + this.attrs.height + modY
	);
	cx.lineTo(
		-center.x - this.origin.x + modX,
		-center.y - this.origin.y + this.attrs.height + modY
	);
	cx.lineTo(
		-center.x - this.origin.x + modX,
		-center.y - this.origin.y + modY
	);
	cx.closePath();
	cx.restore();
};

CanvasRect.prototype.createPointInPath = function(cx) {
	this.createFillPath(cx);
};

//returns center point as relative coordinates from base top left of object
CanvasRect.prototype.center = function() {
	return {
		x: this.attrs.width / 2,
		y: this.attrs.height / 2
	};
},
	
CanvasRect.prototype.box = function() {
	var center, width, height, sinRot, sinRot2;
	if (this.attrs.rotation === 0) {
		center = this.center();
		return {
			top: this.attrs.y - center.y - this.origin.y,
			right: this.attrs.x - center.x - this.origin.x + this.attrs.width,
			bottom: this.attrs.y - center.y - this.origin.y + this.attrs.height,
			left: this.attrs.x - center.x - this.origin.x,
			width: this.attrs.width,
			height: this.attrs.height
		};
	} else {
		center = this.rotatedCenter();
		sinRot = Math.sin(this.attrs.rotation);
		sinRot2 = Math.sin(Math.PI - this.attrs.rotation);
		width = Math.abs(this.attrs.height * sinRot + this.attrs.width * sinRot2);
		height = Math.abs(this.attrs.width * sinRot + this.attrs.height * sinRot2);
		
		return {
			top: Math.round(center.y - height / 2),
			right: Math.round(center.x + width / 2),
			bottom: Math.round(center.y + height / 2),
			left: Math.round(center.x - width / 2),
			width: width,
			height: height
		};
	}
};



//creates text object
sCanvas.fn.text = function() {
	if (arguments.length !== 2 && arguments.length !== 3) {
		throw new Error("Wrong arguments was passed to text().");
	}
	
	try {
		var attrs,
			obj = new CanvasText(this, arguments[0]);
		
		if (arguments.length === 2) {
			attrs = arguments[1];
		} else {
			attrs = {
				x: arguments[1],
				y: arguments[2]
			};
		}

		obj.attr(attrs);

		this.register(obj);
		this.autorepaint();
		
		return obj;
	} catch (err) {
		throw err;
	}
};



var CanvasText = function(sc, text) {
	this.init(sc);
	this.addSupportedAttr(["fontFamily", "fontSize", "fontWeight", "fontStyle", "align", "baseline", "direction"]);
	sCanvas.extend(this.attrHandlers, {
		fontFamily: this.setFont,
		fontSize: this.setFont,
		fontWeight: this.setFont,
		fontStyle: this.setFont
	});
	this.initAttrs();

	try {
		this.val(text);
	} catch (err) {
		throw err;
	}

	return this;
};

CanvasText.prototype = new CanvasObject();

sCanvas.fn.extend(true, {
	defaultAttrs: {
		fontFamily: "sans-serif",
		fontSize: 10,
		align: "left",
		baseline: "alphabetic",
		direction: "inherit"
	}
});

CanvasText.prototype.createFillPath = function(cx) {};

CanvasText.prototype.createStrokePath = function(cx) {};

CanvasText.prototype.createPointInPath = function(cx) {
	var metrics = this.metrics();
	var top = -metrics[this.attrs.baseline];
	var left = this.attrs.align === "left" ? 0
			: (this.attrs.align === "right" ? -metrics.width : (-metrics.width / 2));
	
	cx.save();
	cx.translate(this.attrs.x, this.attrs.y);
	cx.rotate(this.attrs.rotation);
	cx.beginPath();
	cx.moveTo(
		left - this.origin.x,
		top - this.origin.y
	);
	cx.lineTo(
		left - this.origin.x + metrics.width,
		top - this.origin.y
	);
	cx.lineTo(
		left - this.origin.x + metrics.width,
		top - this.origin.y + metrics.height
	);
	cx.lineTo(
		left - this.origin.x,
		top - this.origin.y + metrics.height
	);
	cx.lineTo(
		left - this.origin.x,
		top - this.origin.y
	);
	cx.closePath();
	cx.restore();
};

CanvasText.prototype.draw = function(cx) {
	if (this.isVisible()) {
		cx.font = (this.attrs.fontStyle || "") + " "
				+ (this.attrs.fontWeight || "") + " "
				+ (this.attrs.fontSize ? (this.attrs.fontSize + "px") : "") + " "
				+ (this.attrs.fontFamily || "");
		cx.textAlign = this.attrs.align;
		cx.textBaseline = this.attrs.baseline;
		cx.direction = this.attrs.direction;

		cx.shadowColor = this.attrs.shadowColor;
		cx.shadowBlur = this.attrs.shadowBlur;
		cx.shadowOffsetX = this.attrs.shadowX;
		cx.shadowOffsetY = this.attrs.shadowY;
		cx.globalAlpha = this.attrs.opacity;
		
		cx.save();
		cx.translate(this.attrs.x, this.attrs.y);
		cx.rotate(this.attrs.rotation);
		if (this.attrs.stroke !== null) {
			cx.strokeStyle = this.attrs.stroke;
			cx.strokeText(
				this.textContent,
				-this.origin.x,
				-this.origin.y
			);
		}

		if (this.attrs.fill !== null) {
			cx.fillStyle = this.attrs.fill;
			cx.fillText(
				this.textContent,
				-this.origin.x,
				-this.origin.y
			);
		}
		cx.restore();
	}
};

//stores text
CanvasText.prototype.textContent = null;

//gets/sets text
CanvasText.prototype.val = function(text) {
	if (text === undefined) {
		return this.textContent;
	} else {
		if (typeof text === "string" || typeof text === "number") {
			this.textContent = text;
			
			if (this.span === null) {
				//create span and text
				this.span = document.createElement("span");
				this.spanText = document.createTextNode(text);
				this.spanImg = document.createElement("img");
				
				//the smallest gif - http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
				this.spanImg.src = "";
				this.spanImg.alt = "";
				this.spanImg.width = 1;
				this.spanImg.height = 1;
				
				this.span.appendChild(this.spanText);
				this.span.appendChild(this.spanImg);
				
				//set styles to hide span and font attributes
				this.span.style.position = "absolute";
				this.span.style.top = "0px";
				this.span.style.left = "0px";
				this.span.style.visibility = "hidden";
				this.span.style.fontFamily = this.attrs.fontFamily;
				this.span.style.fontSize = this.attrs.fontSize + "px";
				this.span.style.fontStyle = this.attrs.fontStyle;
				this.span.style.fontWeight = this.attrs.fontWeight;
				
				//append span to wrapper
				this.sc.wrapper.appendChild(this.span);
			} else {
				this.spanText.nodeValue = text;
			}
			
			this.sc.autorepaint();
		} else {
			throw new TypeError("Passed argument must be string or number.");
		}
		
		return this;
	}
};

//sets font attributes
CanvasText.prototype.setFont = function(value, old, attr) {
	this.span.style[attr] = value + (attr === "fontSize" ? "px" : "");
	this.attrs[attr] = value;
};

//stores HTMLSpan node created for measurement purposes
CanvasText.prototype.span = null;

//stores HTMLSpan text node created for measurement purposes
CanvasText.prototype.spanText = null;

//stores HTMLImg node created for measurement purposes
CanvasText.prototype.spanImg = null;

CanvasText.prototype.metrics = function() {
	var i,
		positions = {
			top: "top",
			middle: "middle",
			bottom: "bottom",
			baseline: "alphabetic"
		},
		result = {
			width: this.span.offsetWidth - 1, //subtract 1 for image
			height: this.span.offsetHeight
		};
	
	for (i in positions) {
		this.spanImg.style.verticalAlign = i;
		result[positions[i]] = this.spanImg.offsetTop;
	}
	
	return result;
};

CanvasText.prototype.box = function() {
	var center, metrics, width, height, alignMove,
		sinRot, sinRot2,
		h1, h2, h3, v1, v2, v3,
		result;
	
	metrics = this.metrics();
	
	if (this.attrs.rotation === 0) {
		//distance from center point to left bound
		alignMove = this.attrs.align === "left" ? 0
			: (this.attrs.align === "right" ? metrics.width : (metrics.width / 2));
		
		return {
			top: Math.round(this.attrs.y - this.origin.y - metrics[this.attrs.baseline]),
			right: Math.round(this.attrs.x - this.origin.x + metrics.width - alignMove),
			bottom: Math.round(this.attrs.y - this.origin.y + metrics.height - metrics[this.attrs.baseline]),
			left: Math.round(this.attrs.x - this.origin.x - alignMove),
			width: metrics.width,
			height: metrics.height
		};
	} else {
		//TODO: calculate it directly
		
		//create rect with text dimensions and position
		var rect = this.sc.rect({
			originX: this.origin.x + (this.attrs.align === "left" ? (-metrics.width / 2) : (this.attrs.align === "right" ? (metrics.width / 2) : 0)),
			originY: this.origin.y + metrics[this.attrs.baseline] - metrics.height / 2,
			x: 150 + this.origin.x,
			y: 150 + this.origin.y,
			width: metrics.width,
			height: metrics.height,
			fill: null,
			visible: false,
			rotation: this.attrs.rotation
		});
		
		result = rect.box();
		
		rect.remove();
		
		return result;
		
//		center = this.rotatedCenter();
//		sinRot = Math.sin(this.attrs.rotation);
//		sinRot2 = Math.sin(Math.PI - this.attrs.rotation);
//		
//		if (this.attrs.align === "center") {
//			v1 = sinRot * metrics.width / 2;
//			v2 = sinRot2 * (metrics.height - metrics[this.attrs.baseline]);
//			v3 = sinRot2 * metrics[this.attrs.baseline];
//			
//			h1 = sinRot2 * metrics.width / 2;
//			h2 = sinRot * (metrics.height - metrics[this.attrs.baseline]);
//			h3 = sinRot * metrics[this.attrs.baseline];
//			
//			return {
//				top: Math.round(center.y - v1 - v3),
//				right: Math.round(center.x + h1 + h3),
//				bottom: Math.round(center.y + v1 + v2),
//				left: Math.round(center.x - h1 - h2),
//				width: 2 * h1 + h2 + h3,
//				height: 2 * v1 + v2 + v3
//			};
//		} else if (this.attrs.align === "left") {
//			v1 = sinRot * metrics.width;
//			v2 = sinRot2 * (metrics.height - metrics[this.attrs.baseline]);
//			v3 = sinRot2 * metrics[this.attrs.baseline];
//			
//			h1 = sinRot2 * metrics.width;
//			h2 = sinRot * (metrics.height - metrics[this.attrs.baseline]);
//			h3 = sinRot * metrics[this.attrs.baseline];
//			
//			return {
//				top: Math.round(center.y - v3),
//				right: Math.round(center.x + h1 + h3),
//				bottom: Math.round(center.y + v1 + v2),
//				left: Math.round(center.x - h2),
//				width: h1 + h2 + h3,
//				height: v1 + v2 + v3
//			};
//		} else if (this.attrs.align === "right") {
//			v1 = sinRot * metrics.width;
//			v2 = sinRot2 * (metrics.height - metrics[this.attrs.baseline]);
//			v3 = sinRot2 * metrics[this.attrs.baseline];
//			
//			h1 = sinRot2 * metrics.width;
//			h2 = sinRot * (metrics.height - metrics[this.attrs.baseline]);
//			h3 = sinRot * metrics[this.attrs.baseline];
//			
//			return {
//				top: Math.round(center.y - v1 - v3),
//				right: Math.round(center.x + h3),
//				bottom: Math.round(center.y + v2),
//				left: Math.round(center.x - h1 - h2),
//				width: h1 + h2 + h3,
//				height: v1 + v2 + v3
//			};
//		}
	}
};



//make sCanvas visible
window.sCanvas = sCanvas;

}(window));